feat(tabs): per-tab lockedRepos with independent lock lists#78
Merged
Conversation
Components 1-6 of custom tabs implementation: - CustomTabSchema + BUILTIN_TAB_IDS + isBuiltinTab in schemas.ts - Config CRUD: addCustomTab, updateCustomTab, removeCustomTab, reorderCustomTab - View state: customTabFilters, expandedRepos widened to z.record, dynamic helpers - TabBar: TabId=string, custom tab rendering, + button, edit pencil - DashboardPage: custom tab routing, exclusiveOwnership, visibleIssues/PRs/Runs - Shared filter predicates: isIssueVisible, isPrVisible, isRunVisible - FilterBar: hideOrgRepo prop for custom tab context - 1863 tests (88 new), typecheck clean
- Adds customTabId and filterPreset props to IssuesTab, PullRequestsTab, ActionsTab - activeFilters memo with merge chain: schema defaults → preset → stored - Dispatches filter read/write/reset to customTabFilters or tabFilters - Dynamic tabKey() replaces all expandedRepos dot-notation accesses - Updates createReorderHighlight and createFlashDetection signals - globalFilter bypassed (null) when custom tab active - showScopeFilter returns true for custom tabs - Resolves _self sentinel in user filter preset - DashboardPage passes customTabId and filterPreset to custom tab renders
- CustomTabModal: Kobalte Dialog for create/edit custom tabs - Name, baseType, org/repo scope, filter presets, exclusive toggle - Filter preset fields derived from shared FilterChipGroupDef arrays - Extracts filter group constants to filterTypes.ts (single source) - Wires modal into DashboardPage replacing placeholder - _self sentinel in user filter preset for authenticated user
- CustomTabsSection: table with name, type badge, scope, exclusive, edit/delete/reorder actions - Integrates into SettingsPage after Tabs section - Updates handleExportSettings to include customTabs - Updates defaultTab dropdown to include custom tab options - Delete with window.confirm, disabled add at 10-tab cap
- Adds createEffect to reset all form signals when modal opens with a different editingTab (prevents stale state between edits) - Removes unnecessary orgList memo - Fixes resetCustomTabFilters import source
Security (SEC-001, SEC-002, SEC-006, SEC-008): - Constrains tab ID to alphanumeric/dash/underscore regex - Constrains orgScope entries to REPO_SEGMENT regex - Caps orgScope and repoScope at 100 entries - Prevents ID mutation via updateCustomTab Object.assign Accessibility (UI-002, UI-003, UI-005): - Associates name input and type select with labels (for/id) - Adds aria-expanded and aria-controls to scope collapsible - Adds aria-label to filter preset select dropdowns
- README: adds Custom Tabs section to feature list - USER_GUIDE: adds Custom Tabs subsection with creation, scope, filter presets, exclusivity, and management details
- CustomTabModal: 31 tests covering create/edit mode, validation, base type switching, scope selection, filter presets, exclusive toggle - CustomTabsSection: 27 tests covering table rendering, delete with confirm, reorder, empty state, cap enforcement, edit/add buttons - Total: 1927 tests (152 new from baseline)
Investigated Kobalte Tabs source: keyboard navigation uses
Collection-based delegates and querySelector('[data-key=...]')
for focus management — neither depend on direct children of
Tabs.List. Wrapper divs around custom tab triggers are safe.
Updated comment to reflect verified finding.
- Exclusive issues tab removes claimed items from built-in badge - Exclusive tab shows claimed items in its own badge count - Non-exclusive tabs do not affect built-in tab counts - First exclusive tab wins on overlapping scope - Type isolation: exclusive issues tab does not affect PRs/Actions - 1933 tests total (158 new from baseline)
- PERF-003: hoists Zod filter defaults to module-level constants
(avoids IssueFiltersSchema.parse({}) on every memo invocation)
- Adversarial-3: customTabData only filters the relevant baseType
per tab (was filtering all 3 types unconditionally)
- UI-007: empty state text no longer references hidden + button
- CR-014: createEffect closes modal when editingTabId references a deleted tab (prevents stale edit form) - UI-004: removes role='switch' from exclusive toggle (redundant on native checkbox), adds aria-describedby for description text - Updates test to query by id instead of removed role
- Delete button now shows inline 'Delete? [Yes] [No]' confirmation replacing the action buttons in the table row - Removes window.confirm dependency (blocks main thread, poor a11y, inconsistent with Kobalte Dialog pattern used elsewhere) - Updates tests to verify inline confirmation flow
- Correctness: case-insensitive repoScope matching, stale defaultTab cleanup in loadConfig, tabCounts badge applies filterPreset - Security: updateCustomTab validates via CustomTabSchema.safeParse - Performance: single-pass ignoredItems, visible* short-circuit on empty ownership, customTabData skips non-exclusive inactive tabs, hoisted Zod defaults outside loop - Quality: extract createTabFilterHandlers, formatScopeSummary, module-level baseTypeLabel/baseTypeBadgeClass, typed filterGroupsByType, editingTab createMemo, removed triple blank line - Tests: 37 new tests covering scoping, stale tab fallback, runtime redirect, orphan cleanup, schema validation, customTabId filter presets, scope cascade, hideOrgRepo, badge count integration
- Add scope (involves_me), user (surfacedBy), and sizeCategory filters to tabCounts badge computation for full parity with tab components - Gate checkStatus filter on pr.enriched (matches PullRequestsTab) - Export KNOWN_CONCLUSIONS and KNOWN_EVENTS from filterTypes.ts to eliminate magic array duplication between ActionsTab and DashboardPage - Narrow baseTypeLabel/baseTypeBadgeClass parameter to union type - Add loadConfig stale-defaultTab cleanup tests
- fix PersonalSummaryStrip receiving unfiltered data instead of visible* memos - fix duplicate scope option in CustomTabModal filter preset dropdown - fix removeCustomTab not resetting viewState.lastActiveTab - extract buildTabScopeMatcher to deduplicate scope logic in DashboardPage - extract mergeActiveFilters to deduplicate filter merge in 3 tab components - hoist Zod filter schema defaults to module scope in DashboardPage - simplify CustomTabModal handleSave guard and getPresetValue - fix TabBar bracket notation, section numbering, local type alias - add tests for formatScopeSummary, createTabFilterHandlers, mergeActiveFilters, matchesScope OR semantics, _self sentinel in tabCounts, SettingsPage dropdown
- tabCounts now uses mergeActiveFilters() instead of inline merge (dedup) - add enriched guard to PR role filter in tabCounts (prevents count mismatch during light-phase when reviewerLogins/assigneeLogins are empty) - restore 'All (default)' option for non-scope filter groups in CustomTabModal (fix regression from duplicate scope option removal) - add removeCustomTab lastActiveTab reset tests (matching + non-matching)
- wraps TabBar +/edit and CustomTabsSection action buttons in Tooltip (matches RepoLockControls pattern) - replaces distracting pencil character with subtle cog SVG - changes placeholder from 'My OSAC PRs' to 'Needs Review'
- up/down tooltips show 'Already at top/bottom' when disabled (matches RepoLockControls pattern) - TabBar edit tooltip simplified to 'Edit tab' - TabBar edit icon uses same pencil SVG as CustomTabsSection
- 'Edit' → 'Edit "Tab Name"', 'Delete' → 'Delete "Tab Name"' (matches TrackedUsersSection pattern)
Resolves conflicts in ActionsTab, IssuesTab, and PullRequestsTab by combining upstream empty-locked-repo-row feature (isEmpty guard, EmptyLockedRepoRow fallback, flattened Show structure) with custom-tabs dynamic tabKey() and activeFilters() accessors.
Change lockedRepos from z.array(z.string()) to z.record(z.string(), z.array(z.string())) so each tab (built-in and custom) has independent lock lists and ordering. Schema + migration: 3-shape detection (null→default record, flat array→copy to all 3 built-in tabs, object→passthrough). Cap guard iterates all record entries. Lock functions accept tabKey first param. Components: RepoLockControls, EmptyLockedRepoRow gain tabKey prop. Tab components use viewState.lockedRepos[tabKey()] ?? [] inline. DashboardPage orphan cleanup includes lockedRepos keys. Tests: 2446 passing (+13 net new). Per-tab isolation, cap enforcement, migration shapes, cross-tab independence, reset cleanup.
Three independent reviewers flagged the shared array reference in the flat-array migration path. Spread into independent arrays to prevent latent mutation hazards if migrateLockedRepos is ever called without subsequent Zod parsing.
Verifies that migrateLockedRepos returns independent array copies per tab, not shared references. Addresses CR-005 from code review.
Cap guard now strips non-string array elements before Zod validates, preventing corrupt localStorage entries from rejecting the entire ViewState. Adds tests for custom tab pruning and partial object migration passthrough.
- Extract REPO_STATE_TAB_IDS constant for resetViewState loops - Export LOCKED_REPOS_CAP for test reference (no hardcoded 50) - Fix misleading comment on migrateLockedRepos passthrough - Reformat lock functions + moveTrackedItem to expanded style - Replace algorithm-copy cap-guard test with integration tests - Add custom tab lock mechanics tests for IssuesTab - Add migrateLockedRepos mixed-type array filter test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #76